<?php
namespace Api\Dbal\Doctrine;

use Api\Application;
use Api\Dbal\Doctrine\Serializer\EntitySerializer;
use Api\Http\Response;
use Api\Rest;
use Api\Utils\Arrays;
use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query;
use Doctrine\ORM\Tools\Pagination\Paginator;

/**
 * Class Resource
 *
 * @author Steffen Brem
 * @license MIT License
 * @package Api\Dbal\Doctrine
 * @author Steffen Brem <steffenbrem@gmail.com>
 */
class Resource extends Rest\Resource
{
    const RETURN_QUERY_BUILDER  = 1;
    const RETURN_RESPONSE       = 2;
    const RETURN_RESULT         = 3;

    /**
     * @var string
     */
    private $_alias = 't';

    /**
     * @var string
     */
    protected $entity;

    /**
     * @var array
     */
    protected $identifier = array();

    /**
     * @var EntityManager
     */
    protected $em;

    /**
     * @var EntitySerializer
     */
    protected $serializer;

    /**
     * @param Application $application
     */
    public function __construct(Application $application)
    {
        parent::__construct($application);

        $doctrine = new Doctrine();
        $this->em = $doctrine->getEntityManager();

        $this->serializer = new EntitySerializer($this->em);
    }

    /**
     * @param $qb
     * @param array $fields
     * @param $class
     * @param $alias
     * @return mixed
     * @throws \Exception
     */
    private function _joinTables(&$qb, array $fields, $class, $alias)
    {
        // If we only have 1 field left, return the current alias, because there is no more joining to do
        if (count($fields) <= 1)
        {
            return $alias;
        }

        $field = array_shift($fields);

        $metaData = $this->em->getClassMetadata($class);

        // Check if this field is an association field, otherwise throw an error
        if (!$metaData->hasAssociation($field))
        {
            throw new \Exception("Field '" . $field . "' is not a foreign key for entity '" . $class . "'");
        }

        $mapping = $metaData->getAssociationMapping($field);

        $_alias = $field . count($fields);
        $qb->join($alias . '.' . $field, $_alias);

        return $this->_joinTables($qb, $fields, $mapping['targetEntity'], $_alias);
    }

    /**
     * @param int $return
     * @return Response|array|\Doctrine\ORM\QueryBuilder|mixed|object
     */
    public function __read($return = Resource::RETURN_RESPONSE)
    {
        if (!$this->entity) return;

        $response = array();

        if ($this->request->getIdentifier())
        {
            // We got a custom identifier, use that one instead of the primary key
            if ($this->identifier)
            {
                $pks    = $this->parseIdentifier(array($this->request->getIdentifier()));
                $result = $this->em->getRepository($this->entity)->findOneBy($pks);
            }
            else
            {
                $result = $this->em->getRepository($this->entity)->find($this->request->getIdentifier());
            }
        }
        else
        {
            $qb = $this->em->getRepository($this->entity)->createQueryBuilder($this->_alias);

            // Offset
            $qb->setMaxResults($this->request->getMaxResults());
            $qb->setFirstResult($this->request->getOffset());

            // And where
            foreach ($this->request->getWhere() as $field => $value)
            {
                $alias = $this->_alias;

                // Strip field to check for associated fields
                $parts = explode('.', $field);
                $alias = $this->_joinTables($qb, $parts, $this->entity, 't');

                $field = array_pop($parts);
                $param = count($qb->getParameters());

                if (is_array($value))
                {
                    $qb->andWhere($alias . '.' . $field . ' IN (?' . $param . ')');
                    $qb->setParameter($param, $value);
                }
                else
                {
                    $qb->andWhere($qb->expr()->eq($alias . '.' . $field, '?' . $param));
                    $qb->setParameter($param, $value);
                }
            }

            // Or where
            foreach ($this->request->getOrWhere() as $field => $value)
            {
                $alias = $this->_alias;

                // Strip field to check for associated fields
                $parts = explode('.', $field);
                $alias = $this->_joinTables($qb, $parts, $this->entity, 't');

                $field = array_pop($parts);
                $param = count($qb->getParameters());

                $qb->orWhere($qb->expr()->eq($alias . '.' . $field, '?' . $param));
                $qb->setParameter($param, $value);
            }

            // Order by
            foreach ($this->request->getOrderBy() as $field => $order)
            {
                $alias = $this->_alias;

                // Strip field to check for associated fields
                $parts = explode('.', $field);
                $alias = $this->_joinTables($qb, $parts, $this->entity, 't');

                $field = array_pop($parts);

                $order = ($order == 'd') ? 'DESC' : 'ASC';
                $qb->orderBy($alias . '.' . $field, $order);
            }

            if ($return === self::RETURN_QUERY_BUILDER)
            {
                return $qb;
            }

            // Fetch results
            if ($this->request->getSingleResult())
            {
                $qb->setMaxResults(1);
                $result = $qb->getQuery()->getSingleResult();
            }
            else
            {
                $result = $qb->getQuery()->getResult();
            }

            $paginator = new Paginator($qb);
            $total = count($paginator);
        }

        if ($return === self::RETURN_RESULT)
        {
            return $result;
        }

        if (isset($total))
        {
            $response['rowCount']= $total;
        }

        $result = $this->__serialize($result);

        $response[$this->rootElement] = $result;

        return new Response(Response::OK, $response, $this);
    }

    /**
     * @param $data
     * @return array
     */
    public function __serialize($data)
    {
        return $this->serializer->toArray($data);
    }

    /**
     * Create
     */
    public function __create()
    {
        if (!$this->entity) return;

        $response = array();

        $data = $this->request->getPayload();

        if (Arrays::isAssoc($data))
        {
            $data = array($data);
        }


        foreach ($data as $row)
        {
            $entity = $this->_processEntity($this->entity, $row);

            $this->em->persist($entity);
            $this->em->flush();

            if ($this->request->getReturn())
            {
                $this->em->refresh($entity);
                $response['results'][] = $this->serializer->toArray($entity);
            }
        }

        return new Response(Response::OK, $response, $this);
    }

    /**
     * Update
     */
    public function __update()
    {
        if (!$this->entity) return;

        $response = array();

        $data = $this->request->getPayload();

        if (Arrays::isAssoc($data))
        {
            $data = array($data);
        }

        $metadata = $this->em->getClassMetadata($this->entity);
        $identifierFieldNames = $metadata->getIdentifierFieldNames();

        foreach ($data as $row)
        {
            if ($this->identifier)
            {
                $pks = $this->parsePrimaryKey($this->identifier, $row);
                $entity = $this->em->getRepository($this->entity)->findOneBy($pks);
            }
            else
            {
                $pks = $this->parsePrimaryKey($identifierFieldNames, $row);
                $entity = $this->em->getRepository($this->entity)->find($pks);
            }

            $entity = $this->_processEntity($entity, $row);

            if (!$entity)
            {
                throw new \Exception("Update failed, maybe this is caused by an error.");
            }

            $this->em->persist($entity);
            $this->em->flush();

            if ($this->request->getReturn())
            {
                $response['results'][] = $this->serializer->toArray($entity);
            }
        }

        return new Response(Response::OK, $response, $this);
    }

    /**
     * Delete
     */
    public function __delete()
    {

    }

    /**
     * @param array $identifiers
     * @param array $data
     * @return array|bool
     */
    protected function parsePrimaryKey(array $identifiers, array $data)
    {
        $pks = array();

        foreach ($identifiers as $pk)
        {
            if (!isset($data[$pk]))
            {
                return false;
            }

            $pks[$pk] = $data[$pk];
        }

        return $pks;
    }

    /**
     * Parse identifier with array values
     *
     * @param array $values
     * @return array
     */
    protected function parseIdentifier(array $values)
    {
        $pks = array();

        foreach ($this->identifier as $k => $pk)
        {
            $pks[$pk] = $values[$k];
        }

        return $pks;
    }

    /**
     * Process entity recursively
     *
     * What this class does is it takes a base entity (class name or an entity object itself) and it loops trough the
     * properties of this entity, the associated properties will go to this same method again, but this time with it's
     * own array data. At the end, we will have an fully build entity with it's child entities in it.
     *
     * When the process of the entity is done, simply call $em->merge($entity). This will
     *
     * @param $entityClass
     * @param array $data
     * @return object
     * @throws \Exception
     */
    private function _processEntity($entityClass, array $data)
    {
        $metadata = $this->em->getClassMetadata((is_object($entityClass)) ? get_class($entityClass) : $entityClass);

        // Make sure we got an entity
        $entity = (is_object($entityClass)) ? $entityClass : new $entityClass;

        foreach ($data as $field => $value)
        {
            $method = "set" . ucfirst($field);

            if (is_array($value) && $metadata->hasAssociation($field))
            {
                $assocMapping = $metadata->getAssociationMapping($field);
                $assocClass   = $assocMapping['targetEntity'];

                $assocMetadata = $this->em->getClassMetadata($assocClass);

                // If uuid is set, use it, otherwise check for normal pks
                if (isset($value['uuid']))
                {
                    $pks = array('uuid' => $value['uuid']);
                    $r = $this->em->getRepository($assocClass)->findOneBy($pks);
                }
                else
                {
                    $pks = $this->parsePrimaryKey($assocMetadata->getIdentifierFieldNames(), $value);
                    $r = $this->em->getRepository($assocClass)->find($pks);
                }

                // If no results where found, try to create a new one
                if (!$r)
                {
                    $assocEntity = new $assocClass;
                    $assocEntity = $this->_processEntity($assocEntity, $value);

                    $this->em->persist($assocEntity);

                    if ($this->request->getReturn())
                    {
                        $this->em->flush($assocEntity);
                        $this->em->refresh($assocEntity);
                    }
                }
                else
                {
                    $assocEntity = $this->_processEntity($r, $value);
                }

                if (is_callable(array($entity, $method)))
                {
                    $entity->$method($assocEntity);
                }
            }
            else if ($metadata->hasField($field) || $metadata->hasAssociation($field))
            {
                $mapping = ($metadata->hasAssociation($field))
                    ? $metadata->getAssociationMapping($field)
                    : $metadata->getFieldMapping($field);

                switch ($mapping['type'])
                {
                    case DateTimeType::DATETIME:
                        $value = \DateTime::createFromFormat('Y-m-d\TH:i:s', $value);

                        if (!$value)
                        {
                            $value = null;
                        }

                        break;
                }

                if (is_callable(array($entity, $method)))
                {
                    $entity->$method($value);
                }
            }
        }

        return $entity;
    }
}